﻿#include "myglwidget.h"

//3D空间:
//我们使用多边形和四边形创建3D物体，在这一课里，我们把三角形变为立体的金子塔形状，把四边形变为立方体。

MyGLWidget::MyGLWidget(QWidget *parent) :
    QGLWidget(parent), m_show_full_screen(false), m_rotate_tri(0.0f), m_rotate_quad(0.0f)
{
    showNormal();
    startTimer(50);
}

MyGLWidget::~MyGLWidget()
{
}

//下面的代码的作用是重新设置OpenGL场景的大小，而不管窗口的大小是否已经改变(假定您没有使用全屏模式)。
//甚至您无法改变窗口的大小时(例如您在全屏模式下)，它至少仍将运行一次--在程序开始时设置我们的透视图。
//OpenGL场景的尺寸将被设置成它显示时所在窗口的大小。
void MyGLWidget::resizeGL(int w, int h)
{
    if(h == 0)// 防止被零除
    {
        h = 1;// 将高设为1
    }
    glViewport(0, 0, w, h); //重置当前的视口
    //下面几行为透视图设置屏幕。意味着越远的东西看起来越小。这么做创建了一个现实外观的场景。
    //此处透视按照基于窗口宽度和高度的45度视角来计算。0.1f，100.0f是我们在场景中所能绘制深度的起点和终点。
    //glMatrixMode(GL_PROJECTION)指明接下来的两行代码将影响projection matrix(投影矩阵)。
    //投影矩阵负责为我们的场景增加透视。 glLoadIdentity()近似于重置。它将所选的矩阵状态恢复成其原始状态。
    //调用glLoadIdentity()之后我们为场景设置透视图。
    //glMatrixMode(GL_MODELVIEW)指明任何新的变换将会影响 modelview matrix(模型观察矩阵)。
    //模型观察矩阵中存放了我们的物体讯息。最后我们重置模型观察矩阵。如果您还不能理解这些术语的含义，请别着急。
    //在以后的教程里，我会向大家解释。只要知道如果您想获得一个精彩的透视场景的话，必须这么做。
    glMatrixMode(GL_PROJECTION);// 选择投影矩阵
    glLoadIdentity();// 重置投影矩阵
    //设置视口的大小
    gluPerspective(45.0f,(GLfloat)w/(GLfloat)h,0.1f,100.0f);

    glMatrixMode(GL_MODELVIEW);	//选择模型观察矩阵
    glLoadIdentity(); // 重置模型观察矩阵
}

//接下的代码段中，我们将对OpenGL进行所有的设置。我们将设置清除屏幕所用的颜色，打开深度缓存，启用smooth shading(阴影平滑)，等等。
//这个例程直到OpenGL窗口创建之后才会被调用。此过程将有返回值。但我们此处的初始化没那么复杂，现在还用不着担心这个返回值。
void MyGLWidget::initializeGL()
{
    //下一行启用smooth shading(阴影平滑)。阴影平滑通过多边形精细的混合色彩，并对外部光进行平滑。
    //我将在另一个教程中更详细的解释阴影平滑。
    glShadeModel(GL_SMOOTH); // 启用阴影平滑

    //下一行设置清除屏幕时所用的颜色。如果您对色彩的工作原理不清楚的话，我快速解释一下。
    //色彩值的范围从0.0f到1.0f。0.0f代表最黑的情况，1.0f就是最亮的情况。
    //glClearColor 后的第一个参数是Red Intensity(红色分量),第二个是绿色，第三个是蓝色。
    //最大值也是1.0f，代表特定颜色分量的最亮情况。最后一个参数是Alpha值。当它用来清除屏幕的时候，我们不用关心第四个数字。
    //现在让它为0.0f。我会用另一个教程来解释这个参数。
    //通过混合三种原色(红、绿、蓝)，您可以得到不同的色彩。希望您在学校里学过这些。
    //因此，当您使用glClearColor(0.0f,0.0f,1.0f,0.0f)，您将用亮蓝色来清除屏幕。
    //如果您用 glClearColor(0.5f,0.0f,0.0f,0.0f)的话，您将使用中红色来清除屏幕。不是最亮(1.0f)，也不是最暗 (0.0f)。
    //要得到白色背景，您应该将所有的颜色设成最亮(1.0f)。要黑色背景的话，您该将所有的颜色设为最暗(0.0f)。
    glClearColor(0.0f, 0.0f, 0.0f, 0.0f); // 黑色背景

    //接下来的三行必须做的是关于depth buffer(深度缓存)的。将深度缓存设想为屏幕后面的层。
    //深度缓存不断的对物体进入屏幕内部有多深进行跟踪。
    //我们本节的程序其实没有真正使用深度缓存，但几乎所有在屏幕上显示3D场景OpenGL程序都使用深度缓存。
    //它的排序决定那个物体先画。这样您就不会将一个圆形后面的正方形画到圆形上来。深度缓存是OpenGL十分重要的部分。
    glClearDepth(1.0f);	// 设置深度缓存
    glEnable(GL_DEPTH_TEST); // 启用深度测试
    glDepthFunc(GL_LEQUAL);	// 所作深度测试的类型

    //接着告诉OpenGL我们希望进行最好的透视修正。这会十分轻微的影响性能。但使得透视图看起来好一点。
    glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);	// 告诉系统对透视进行修正
}

//在上节课的内容上作些扩展，我们现在开始生成真正的3D对象，而不是象前两节课中那样3D世界中的2D对象。
//我们给三角形增加一个左侧面，一个右侧面，一个后侧面来生成一个金字塔(四棱锥)。给正方形增加左、右、上、下及背面生成一个立方体。
//我们混合金字塔上的颜色，创建一个平滑着色的对象。给立方体的每一面则来个不同的颜色。
void MyGLWidget::paintGL()
{
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);	// 清除屏幕及深度缓存
    glLoadIdentity();					// 重置模型观察矩阵
    glTranslatef(-1.5f,0.0f,-6.0f);				// 左移 1.5 单位，并移入屏幕 6.0
    glRotatef(m_rotate_tri,0.0f,1.0f,0.0f);				// Y轴旋转金字塔
    glBegin(GL_TRIANGLES);					// 开始绘制金字塔的各个面
    //有些人可能早已在上节课中的代码上尝试自行创建3D对象了。
    //但经常有人来信问我:"我的对象怎么不会绕着其自身的轴旋转？看起来总是在满屏乱转。
    //"要让您的对象绕自身的轴旋转，您必须让对象的中心坐标总是(0.0f,0,0f,0,0f)。
    //下面的代码创建一个绕者其中心轴旋转的金字塔。金字塔的上顶点高出原点一个单位，底面中心低于原点一个单位。
    //上顶点在底面的投影位于底面的中心。
    //注意所有的面－三角形都是逆时针次序绘制的。这点十分重要，在以后的课程中我会作出解释。
    //现在，您只需明白要么都逆时针，要么都顺时针，但永远不要将两种次序混在一起，除非您有足够的理由必须这么做。
        glColor3f(1.0f,0.0f,0.0f);			// 红色
        glVertex3f( 0.0f, 1.0f, 0.0f);			// 三角形的上顶点 (前侧面)
        glColor3f(0.0f,1.0f,0.0f);			// 绿色
        glVertex3f(-1.0f,-1.0f, 1.0f);			// 三角形的左下顶点 (前侧面)
        glColor3f(0.0f,0.0f,1.0f);			// 蓝色
        glVertex3f( 1.0f,-1.0f, 1.0f);			// 三角形的右下顶点 (前侧面)
    //现在绘制右侧面。注意其底边上的两个顶点的X坐标位于中心右侧的一个单位处。
    //顶点则位于Y轴上的一单位处，且Z坐标正好处于底边的两顶点的Z坐标中心。右侧面从上顶点开始向外侧倾斜至底边上。
    //这次的左下顶点用蓝色绘制，以保持与前侧面的右下顶点的一致。蓝色将从这个角向金字塔的前侧面和右侧面扩展并与其他颜色混合。
    //还应注意到后面的三个侧面和前侧面处于同一个glBegin(GL_TRIANGLES) 和 glEnd()语句中间。
    //因为我们是通过三角形来构造这个金字塔的。OpenGL知道每三个点构成一个三角形。
    //当它画完一个三角形之后，如果还有余下的点出现，它就以为新的三角形要开始绘制了。
    //OpenGL在这里并不会将四点画成一个四边形，而是假定新的三角形开始了。所以千万不要无意中增加任何多余的点。
        glColor3f(1.0f,0.0f,0.0f);			// 红色
        glVertex3f( 0.0f, 1.0f, 0.0f);			// 三角形的上顶点 (右侧面)
        glColor3f(0.0f,0.0f,1.0f);			// 蓝色
        glVertex3f( 1.0f,-1.0f, 1.0f);			// 三角形的左下顶点 (右侧面)
        glColor3f(0.0f,1.0f,0.0f);			// 绿色
        glVertex3f( 1.0f,-1.0f, -1.0f);			// 三角形的右下顶点 (右侧面)
    //现在是后侧面。再次切换颜色。左下顶点又回到绿色，因为后侧面与右侧面共享这个角。
        glColor3f(1.0f,0.0f,0.0f);			// 红色
        glVertex3f( 0.0f, 1.0f, 0.0f);			// 三角形的上顶点 (后侧面)
        glColor3f(0.0f,1.0f,0.0f);			// 绿色
        glVertex3f( 1.0f,-1.0f, -1.0f);			// 三角形的左下顶点 (后侧面)
        glColor3f(0.0f,0.0f,1.0f);			// 蓝色
        glVertex3f(-1.0f,-1.0f, -1.0f);			// 三角形的右下顶点 (后侧面)
    //最后画左侧面。又要切换颜色。左下顶点是蓝色，与后侧面的右下顶点相同。右下顶点是蓝色，与前侧面的左下顶点相同。
    //到这里金字塔就画完了。因为金字塔只绕着Y轴旋转，我们永远都看不见底面，因而没有必要添加底面。
    //如果您觉得有经验了，尝试增加底面(正方形)，并将金字塔绕X轴旋转来看看您是否作对了。确保底面四个顶点的颜色与侧面的颜色相匹配。
        glColor3f(1.0f,0.0f,0.0f);			// 红色
        glVertex3f( 0.0f, 1.0f, 0.0f);			// 三角形的上顶点 (左侧面)
        glColor3f(0.0f,0.0f,1.0f);			// 蓝色
        glVertex3f(-1.0f,-1.0f,-1.0f);			// 三角形的左下顶点 (左侧面)
        glColor3f(0.0f,1.0f,0.0f);			// 绿色
        glVertex3f(-1.0f,-1.0f, 1.0f);			// 三角形的右下顶点 (左侧面)
    glEnd();						// 金字塔绘制结束

    //接下来开始画立方体。他由六个四边形组成。所有的四边形都以逆时针次序绘制。就是说先画右上角，然后左上角、左下角、最后右下角。
    //您也许认为画立方体的背面的时候这个次序看起来好像顺时针，但别忘了我们从立方体的背后看背面的时候，与您现在所想的正好相反。
    //(译者注：您是从立方体的外面来观察立方体的)。
    //注意到这次我们将立方体移地更远离屏幕了。因为立方体的大小要比金字塔大，同样移入6个单位时，立方体看起来要大的多。
    //这是透视的缘故。越远的对象看起来越小 :) 。
    glLoadIdentity();
    glTranslatef(1.5f,0.0f,-7.0f);				// 先右移再移入屏幕
    glRotatef(m_rotate_quad,1.0f,1.0f,1.0f);			// XYZ轴上旋转立方体
    glBegin(GL_QUADS);					// 开始绘制立方体
    //先画立方体的顶面。从中心上移一单位，注意Y坐标始终为一单位，表示这个四边形与Z轴平行。
    //先画右上顶点，向右一单位，再屏幕向里一单位。然后左上顶点，向左一单位，再屏幕向里一单位。
    //然后是靠近观察者的左下和右下顶点。就是屏幕往外一单位。
        glColor3f(0.0f,1.0f,0.0f);			// 颜色改为蓝色
        glVertex3f( 1.0f, 1.0f,-1.0f);			// 四边形的右上顶点 (顶面)
        glVertex3f(-1.0f, 1.0f,-1.0f);			// 四边形的左上顶点 (顶面)
        glVertex3f(-1.0f, 1.0f, 1.0f);			// 四边形的左下顶点 (顶面)
        glVertex3f( 1.0f, 1.0f, 1.0f);			// 四边形的右下顶点 (顶面)
    //底面的画法和顶面十分类似。只是Y坐标变成了－1。如果我们从立方体的下面来看立方体的话，您会注意到右上角离观察者最近，
    //因此我们先画离观察者最近的顶点。然后是左上顶点最后才是屏幕里面的左下和右下顶点。
    //如果您真的不在乎绘制多边形的次序(顺时针或者逆时针)的话，您可以直接拷贝顶面的代码，将Y坐标从1改成 -1，也能够工作。
    //但一旦您进入象纹理映射这样的领域时，忽略绘制次序会导致十分怪异的结果。
        glColor3f(1.0f,0.5f,0.0f);			// 颜色改成橙色
        glVertex3f( 1.0f,-1.0f, 1.0f);			// 四边形的右上顶点(底面)
        glVertex3f(-1.0f,-1.0f, 1.0f);			// 四边形的左上顶点(底面)
        glVertex3f(-1.0f,-1.0f,-1.0f);			// 四边形的左下顶点(底面)
        glVertex3f( 1.0f,-1.0f,-1.0f);			// 四边形的右下顶点(底面)
    //接着画立方体的前面。保持Z坐标为一单位，前面正对着我们。
        glColor3f(1.0f,0.0f,0.0f);			// 颜色改成红色
        glVertex3f( 1.0f, 1.0f, 1.0f);			// 四边形的右上顶点(前面)
        glVertex3f(-1.0f, 1.0f, 1.0f);			// 四边形的左上顶点(前面)
        glVertex3f(-1.0f,-1.0f, 1.0f);			// 四边形的左下顶点(前面)
        glVertex3f( 1.0f,-1.0f, 1.0f);			// 四边形的右下顶点(前面)
    //立方体后面的绘制方法与前面类似。只是位于屏幕的里面。注意Z坐标现在保持 -1 不变。
        glColor3f(1.0f,1.0f,0.0f);			// 颜色改成黄色
        glVertex3f( 1.0f,-1.0f,-1.0f);			// 四边形的右上顶点(后面)
        glVertex3f(-1.0f,-1.0f,-1.0f);			// 四边形的左上顶点(后面)
        glVertex3f(-1.0f, 1.0f,-1.0f);			// 四边形的左下顶点(后面)
        glVertex3f( 1.0f, 1.0f,-1.0f);			// 四边形的右下顶点(后面)
    //还剩两个面就完成了。您会注意到总有一个坐标保持不变。这一次换成了X坐标。因为我们在画左侧面。
        glColor3f(0.0f,0.0f,1.0f);			// 颜色改成蓝色
        glVertex3f(-1.0f, 1.0f, 1.0f);			// 四边形的右上顶点(左面)
        glVertex3f(-1.0f, 1.0f,-1.0f);			// 四边形的左上顶点(左面)
        glVertex3f(-1.0f,-1.0f,-1.0f);			// 四边形的左下顶点(左面)
        glVertex3f(-1.0f,-1.0f, 1.0f);			// 四边形的右下顶点(左面)
    //立方体的最后一个面了。X坐标保持为一单位。逆时针绘制。您愿意的话，留着这个面不画也可以，这样就是一个盒子:)
    //或者您要是有兴趣可以改变立方体所有顶点的色彩值，象金字塔那样混合颜色。
    //您会看见一个非常漂亮的彩色立方体，各种颜色在它的各个表面流淌。
        glColor3f(1.0f,0.0f,1.0f);			// 颜色改成紫罗兰色
        glVertex3f( 1.0f, 1.0f,-1.0f);			// 四边形的右上顶点(右面)
        glVertex3f( 1.0f, 1.0f, 1.0f);			// 四边形的左上顶点(右面)
        glVertex3f( 1.0f,-1.0f, 1.0f);			// 四边形的左下顶点(右面)
        glVertex3f( 1.0f,-1.0f,-1.0f);			// 四边形的右下顶点(右面)
    glEnd();						// 立方体绘制结束

}

void MyGLWidget::keyPressEvent(QKeyEvent *event)
{
    switch(event->key())
    {
        case Qt::Key_F2:
        {
            m_show_full_screen = !m_show_full_screen;
            if(m_show_full_screen)
            {
                showFullScreen();
            }
            else
            {
                showNormal();
            }
            updateGL();
            break;
        }
        case Qt::Key_Escape:
        {
            qApp->exit();
            break;
        }
    }
}

void MyGLWidget::timerEvent(QTimerEvent *event)
{
    //下两行是新增的。倘若把 rtri 和 rquad 想象为容器，那么在程序的开始我们创建了容器( GLfloat rtri , 和 GLfloat rquad )。
    //当容器创建之后，里面是空的。下面的第一行代码是向容器中添加0.2。
    //因此每次当我们运行完前面的代码后，都会在这里使 rtri 容器中的值增长0.2。
    //后面一行将 rquad 容器中的值减少0.15。同样每次当我们运行完前面的代码后，都会在这里使 rquad 容器中的值下跌0.15。
    //下跌最终会导致对象旋转的方向和增长的方向相反。
    //尝试改变下面代码中的+和-，来体会对象旋转的方向是如何改变的。
    //并试着将0.2改成1.0。这个数字越大，物体就转的越快，这个数字越小，物体转的就越慢。
    m_rotate_tri+=0.2f;						// 增加三角形的旋转变量
    m_rotate_quad-=0.15f;						// 减少四边形的旋转变量
    updateGL();
    QGLWidget::timerEvent(event);
}

//这一课又结束了。到这里您应该已经较好的掌握了在3D空间创建对象的方法。必须将OpenGL屏幕想象成一张很大的画纸，后面还带着许多透明的层。
//差不多就是个由大量的点组成的立方体。这些点从左至右、从上至下、从前到后的布满了这个立方体。
//如果您能想象的出在屏幕的深度方向，应该在设计新3D对象时没有任何问题。
//如果您对3D空间的理解很困难的话，千万不要灰心! 刚开始的时候，领会这些内容会很难。象立方体这样的对象是您练习的好例子。
//继续努力吧！如果您有什么意见或建议请给我EMAIL。如果您认为有什么不对或可以改进，请告诉我。我想做最好的OpenGL教程并对您的反馈感兴趣。
